/**
 * Copyright 2021 xpstem.com
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.xpstem.lelink.operation;

import com.xpstem.lelink.enums.InputPort;
import com.xpstem.lelink.enums.Layer;
import com.xpstem.lelink.enums.OpCode;
import com.xpstem.lelink.type.*;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;

/**
 * EV3 input operations.
 * @author billy zhang
 */
public class EV3InputOpFactory {

    private static final short CMD_DEVICE_GET_FORMAT = 0x02;
    private static final short CMD_DEVICE_GET_TYPEMODE = 0x05;
    private static final short CMD_DEVICE_READY_PCT = 0x1B;
    private static final short CMD_DEVICE_READY_RAW = 0x1C;
    private static final short CMD_DEVICE_READY_SI = 0x1D;
    private static final short CMD_DEVICE_CLR_ALL = 0x0A;
    private static final short CMD_DEVICE_GET_RAW = 0x0B;
    private static final short CMD_DEVICE_GET_CONNECTION = 0x0C;
    private static final short CMD_DEVICE_STOP_ALL = 0x0D;
    private static final short CMD_DEVICE_GET_NAME = 0x15;
    private static final short CMD_DEVICE_GET_MODENAME = 0x16;


    public static final short PORT_EMPTY_OR_UNAVAILABLE = 0x7E;

    private EV3InputOpFactory() {
    }

    /**
     * Enables a program to read all available devices on input chain
     *
     * @param length  (Data8) LENGTH – Maximum number of devices types (Normally 32)
     *                Returns
     * @param array   (Data8) ARRAY – First element of data8 array of types (Normally 32)
     * @param changed (Data8) CHANGED – Changed status 有变化的端口数量？
     */
    public static EV3Operation opInputDeviceList(
            Data8 length, ReturnParam<Data8> array, ReturnParam<Data8> changed) throws IOException {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

        LCEncoder.from(length).lc0(outputStream);
        GVEncoder.from(array.getIndex()).gvx(outputStream);
        GVEncoder.from(changed.getIndex()).gvx(outputStream);
        byte[] opData = outputStream.toByteArray();

        List<ReturnParam<? extends EV3Type>> params = Arrays.asList(array, changed);
        int gvSize = params.stream().mapToInt(ReturnParam::getSize).sum();

        EV3OperationPackage opPkg = new EV3OperationPackage(OpCode.INPUT_DEVICE_LIST, opData, gvSize);
        return new EV3Operation(opPkg, params );
    }

    /**
     * Read information about external sensor device
     */
    public static class OpInputDevice {

        private OpInputDevice() {
        }

        /**
         *
         * @param layer (Data8) LAYER – Specify chain layer number [0-3]
         * @param no (Data8) NO – Port number
         * @param datasets (Data8) Datasets – Number of data sets
         * @param format (Data8) FORMAT – Format [0-3], (0: 8-bit, 1: 16-bit, 2: 32-bit, 3: Float point)
         * @param modes (Data8) MODES – Number of modes [1-8]
         * @param view (Data8) VIEW – Number of modes visible within port view app [1-8]
         */
        public static EV3Operation getFormat(
                Layer layer,
                 InputPort no,
                 ReturnParam<Data8> datasets,
                 ReturnParam<Data8> format,
                ReturnParam<Data8> modes,
                ReturnParam<Data8> view) throws IOException {

            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

            LCEncoder.from(CMD_DEVICE_GET_FORMAT).lc0(outputStream);
            LCEncoder.from(layer).lc0(outputStream);
            LCEncoder.from(no).lc0(outputStream);
            GVEncoder.from(datasets.getIndex()).gvx(outputStream);
            GVEncoder.from(format.getIndex()).gvx(outputStream);
            GVEncoder.from(modes.getIndex()).gvx(outputStream);
            GVEncoder.from(view.getIndex()).gvx(outputStream);
            byte[] opData = outputStream.toByteArray();

            List<ReturnParam<? extends EV3Type>> params = Arrays.asList(datasets, format, modes, view);
            int gvSize = params.stream().mapToInt(ReturnParam::getSize).sum();

            EV3OperationPackage opPkg = new EV3OperationPackage(OpCode.INPUT_DEVICE, opData, gvSize);
            return new EV3Operation(opPkg, params, "Input_Device_GetFormat");
        }

        /**
         *
         * @param layer (Data8) LAYER – Specify chain layer number [0-3]
         * @param no (Data8) NO – Port number
         * @param type (Data8) TYPE – See device type list (Please reference section 0)
         * @param mode (Data8) MODE – Device mode [0-7]
         */
        public static EV3Operation getTypeMode(
                Layer layer,
                InputPort no,
                ReturnParam<Data8> type,
                ReturnParam<Data8> mode) throws IOException {

            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

            LCEncoder.from(CMD_DEVICE_GET_TYPEMODE).lc0(outputStream);
            LCEncoder.from(layer).lc0(outputStream);
            LCEncoder.from(no).lc0(outputStream);
            GVEncoder.from(type.getIndex()).gvx(outputStream);
            GVEncoder.from(mode.getIndex()).gvx(outputStream);
            byte[] opData = outputStream.toByteArray();

            List<ReturnParam<? extends EV3Type>> params = Arrays.asList(type, mode);
            int gvSize = params.stream().mapToInt(ReturnParam::getSize).sum();

            EV3OperationPackage opPkg = new EV3OperationPackage(OpCode.INPUT_DEVICE, opData, gvSize);
            return new EV3Operation(opPkg, params, "Input_Device_GetTypeMode");
        }

        /**
         *
         * @param layer (Data8) LAYER – Specify chain layer number [0-3]
         * @param no (Data8) NO – Port number
         * @param type (Data8) TYPE – Specify device type (0 = Don’t change type)
         * @param mode (Data8) MODE – Device mode [0-7] (-1 = Don’t change mode)
         * @param values (Data8) VALUES – Number of return values
         * @param valueArray (Data8) VALUE1 – First value received from sensor in the specified mode
         * Returns (Depending on number of data samples requested in (VALUES))
         */
        public static EV3Operation readyPct(
                Layer layer,
                InputPort no,
                Data8 type,
                Data8 mode,
                Data8 values,
                ReturnParam<Data8> valueArray) throws IOException {

            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

            LCEncoder.from(CMD_DEVICE_READY_PCT).lc0(outputStream);
            LCEncoder.from(layer).lc0(outputStream);
            LCEncoder.from(no).lc0(outputStream);
            LCEncoder.from(type).lcx(outputStream);
            LCEncoder.from(mode).lcx(outputStream);
            LCEncoder.from(values).lcx(outputStream);
            GVEncoder.from(valueArray.getIndex()).gvx(outputStream);
            byte[] opData = outputStream.toByteArray();

            EV3OperationPackage opPkg = new EV3OperationPackage(OpCode.INPUT_DEVICE, opData, valueArray.getSize());
            return new EV3Operation(opPkg, Arrays.asList(valueArray), "Input_Device_ReadyPct");
        }

        /**
         *
         * @param layer (Data8) LAYER – Specify chain layer number [0-3]
         * @param no (Data8) NO – Port number
         * @param type (Data8) TYPE – Specify device type (0 = Don’t change type)
         * @param mode (Data8) MODE – Device mode [0-7] (-1 = Don’t change mode)
         * @param values (Data8) VALUES – Number of return values
         * @param valueArray (Data32) VALUE1 – First value received from sensor in the specified mode
         * Returns (Depending on number of data samples requested in (VALUES))
         */
        public static EV3Operation readyRaw(
                Layer layer,
                InputPort no,
                Data8 type,
                Data8 mode,
                Data8 values,
                ReturnParam<Data32> valueArray) throws IOException {

            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

            LCEncoder.from(CMD_DEVICE_READY_RAW).lc0(outputStream);
            LCEncoder.from(layer).lc0(outputStream);
            LCEncoder.from(no).lc0(outputStream);
            LCEncoder.from(type).lcx(outputStream);
            LCEncoder.from(mode).lcx(outputStream);
            LCEncoder.from(values).lcx(outputStream);
            GVEncoder.from(valueArray.getIndex()).gvx(outputStream);
            byte[] opData = outputStream.toByteArray();

            EV3OperationPackage opPkg = new EV3OperationPackage(OpCode.INPUT_DEVICE, opData, valueArray.getSize());
            return new EV3Operation(opPkg, Arrays.asList(valueArray), "Input_Device_ReadyRaw");
        }

        /**
         *
         * @param layer  (Data8) LAYER – Specify chain layer number [0-3]
         * @param no (Data8) NO – Port number
         * @param type   (Data8) TYPE – Specify device type (0 = Don’t change type)
         * @param mode   (Data8) MODE – Device mode [0-7] (-1 = Don’t change mode)
         * @param values (Data8) VALUES – Number of return values
         * @param valueArray (DataF) VALUE1 – First value received from sensor in the specified mode
         * Returns (Depending on number of data samples requested in (VALUES))
         */
        public static EV3Operation readySi(
                Layer layer,
                InputPort no,
                Data8 type,
                Data8 mode,
                Data8 values,
                ReturnParam<DataF> valueArray) throws IOException {

            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

            LCEncoder.from(CMD_DEVICE_READY_SI).lc0(outputStream);
            LCEncoder.from(layer).lc0(outputStream);
            LCEncoder.from(no).lc0(outputStream);
            LCEncoder.from(type).lcx(outputStream);
            LCEncoder.from(mode).lcx(outputStream);
            LCEncoder.from(values).lcx(outputStream);
            GVEncoder.from(valueArray.getIndex()).gvx(outputStream);
            byte[] opData = outputStream.toByteArray();

            EV3OperationPackage opPkg = new EV3OperationPackage(OpCode.INPUT_DEVICE, opData, valueArray.getSize());
            return new EV3Operation(opPkg, Arrays.asList(valueArray), "Input_Device_ReadySi");

        }

        /**
         * Clear all device counters and values
         * @param layer (Data8) LAYER – Specify chain layer number [0-3] (-1 = All)
         */
        public static EV3Operation clrAll(
                Layer layer) throws IOException {

            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

            LCEncoder.from(CMD_DEVICE_CLR_ALL).lc0(outputStream);
            LCEncoder.from(layer).lc0(outputStream);
            byte[] opData = outputStream.toByteArray();

            EV3OperationPackage opPkg = new EV3OperationPackage(OpCode.INPUT_DEVICE, opData);
            return new EV3Operation(opPkg, "Input_Device_ClrAll");

        }

        /**
         *
         * @param layer (Data8) LAYER – Specify chain layer number [0-3]
         * @param no (Data8) NO – Port number
         * @param value (Data32) Value – 32 bit raw value on the given sensor port
         */
        public static EV3Operation getRaw(
                Layer layer,
                InputPort no,
                ReturnParam<Data32> value) throws IOException {

            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

            LCEncoder.from(CMD_DEVICE_GET_RAW).lc0(outputStream);
            LCEncoder.from(layer).lc0(outputStream);
            LCEncoder.from(no).lc0(outputStream);
            GVEncoder.from(value.getIndex()).gvx(outputStream);
            byte[] opData = outputStream.toByteArray();

            EV3OperationPackage opPkg = new EV3OperationPackage(OpCode.INPUT_DEVICE, opData, value.getSize());
            return new EV3Operation(opPkg, Arrays.asList(value), "Input_Device_GetRaw");
        }

        /**
         *
         * Connection type:
         * 0x6F: CONN_UNKNOW : Fake
         * 0x75: CONN_DAISYCHAIN : Daisy chained
         * 0x76: CONN_NXT_COLOR : NXT Color sensor
         * 0x77: CONN_NXT_DUMB : NXT analog sensor
         * 0x78: CONN_NXT_IIC : NXT IIC sensor
         * 0x79: CONN_INPUT_DUMB : EV3 input device with ID resistor
         * 0x7A: CONN_INPUT_UART : EV3 input UART sensor
         * 0x7B: CONN_OUTPUT_DUMB : EV3 output device with ID resistor
         * 0x7C: CONN_OUTPUT_INTELLIGENT : EV3 output device with communication
         * 0x7D: CONN_OUTPUT_TACHO : EV3 Tacho motor with ID resistor
         * 0x7E: CONN_NONE : Port empty or not available
         * 0x7F: CONN_ERROR : Port not empty and type is invalid
         *
         * @param layer
         * @param port
         */
        public static EV3Operation getConnection(
                Layer layer,
                InputPort port,
                ReturnParam<Data8> conn) throws IOException {

            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

            LCEncoder.from(CMD_DEVICE_GET_CONNECTION).lc0(outputStream);
            LCEncoder.from(layer.value()).lc0(outputStream);
            LCEncoder.from(port).lc0(outputStream);
            GVEncoder.from(conn.getIndex()).gvx(outputStream);
            byte[] opData = outputStream.toByteArray();

            EV3OperationPackage opPkg = new EV3OperationPackage(OpCode.INPUT_DEVICE, opData, conn.getSize());
            return new EV3Operation(opPkg, Arrays.asList(conn), "Input_Device_ClrAll");

        }

        public static EV3Operation stopAll(
                Layer layer) throws IOException {

            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

            LCEncoder.from(CMD_DEVICE_STOP_ALL).lc0(outputStream);
            LCEncoder.from(layer.value()).lc0(outputStream);
            byte[] opData = outputStream.toByteArray();

            EV3OperationPackage opPkg = new EV3OperationPackage(OpCode.INPUT_DEVICE, opData);
            return new EV3Operation(opPkg, "Input_Device_StopAll");

        }

        public static EV3Operation getName(
                Layer layer,
                InputPort port,
                Data8 length,
                ReturnParam<DataS> name) throws IOException {

            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

            LCEncoder.from(CMD_DEVICE_GET_NAME).lc0(outputStream);
            LCEncoder.from(layer.value()).lc0(outputStream);
            LCEncoder.from(port).lc0(outputStream);
            LCEncoder.from(length).lcx(outputStream);
            GVEncoder.from(name.getIndex()).gvx(outputStream);
            byte[] opData = outputStream.toByteArray();

            EV3OperationPackage opPkg = new EV3OperationPackage(OpCode.INPUT_DEVICE, opData, name.getSize());
            return new EV3Operation(opPkg, Arrays.asList(name), "Input_Device_GetName");

        }

        public static EV3Operation getModeName(
                Layer layer,
                InputPort port,
                Data8 mode,
                Data8 length,
                ReturnParam<DataS> name) throws IOException {

            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

            LCEncoder.from(CMD_DEVICE_GET_MODENAME).lc0(outputStream);
            LCEncoder.from(layer.value()).lc0(outputStream);
            LCEncoder.from(port).lc0(outputStream);
            LCEncoder.from(mode).lcx(outputStream);
            LCEncoder.from(length).lcx(outputStream);
            GVEncoder.from(name.getIndex()).gvx(outputStream);
            byte[] opData = outputStream.toByteArray();

            EV3OperationPackage opPkg = new EV3OperationPackage(OpCode.INPUT_DEVICE, opData, name.getSize());
            return new EV3Operation(opPkg, Arrays.asList(name), "Input_Device_GetModeName");

        }

    }

    /**
     * This function enables reading specific device and mode in percentage
     *
     * @param layer (Data8) LAYER – Specify chain layer number [0-3]
     * @param no (Data8) NO – Port number
     * @param type (Data8) TYPE – Specify device type (0 = Don’t change type)
     * @param mode (Data8) MODE – Device mode [0-7] (-1 = Don’t change mode)
     */
    public static EV3Operation opInputRead(
            Layer layer,
            InputPort no,
            Data8 type,
            Data8 mode,
            ReturnParam<Data8> pct) throws IOException {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

        LCEncoder.from(layer.value()).lc0(outputStream);
        LCEncoder.from(no).lc0(outputStream);
        LCEncoder.from(type).lcx(outputStream);
        LCEncoder.from(mode).lcx(outputStream);
        GVEncoder.from(pct.getIndex()).gvx(outputStream);
        byte[] opData = outputStream.toByteArray();

        EV3OperationPackage opPkg = new EV3OperationPackage(OpCode.INPUT_READ, opData, pct.getSize());
        return new EV3Operation(opPkg, Arrays.asList(pct) );
    }

    /**
     * This function enables testing if a sensor is busy changing type or mode
     *
     * @param layer (Data8) LAYER – Specify chain layer number [0 - 3]
     * @param no (Data8) NO – Port number
     */
    public static EV3Operation opInputTest(
            Layer layer,
            InputPort no,
            ReturnParam<Data8> busy) throws IOException {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

        LCEncoder.from(layer).lc0(outputStream);
        LCEncoder.from(no).lc0(outputStream);
        GVEncoder.from(busy.getIndex()).gvx(outputStream);
        byte[] opData = outputStream.toByteArray();

        EV3OperationPackage opPkg = new EV3OperationPackage(OpCode.INPUT_TEST, opData, busy.getSize());
        return new EV3Operation(opPkg, Arrays.asList(busy) );
    }

    /**
     * This function enables a program to wait for a device to be ready (Wait for valid data)
     *
     * @param layer (Data8) LAYER – Specify chain layer number [0 - 3]
     * @param no (Data8) NO – Port number
     */
    public static EV3Operation opInputReady(
            Layer layer,
            InputPort no) throws IOException {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

        LCEncoder.from(layer.value()).lc0(outputStream);
        LCEncoder.from(no).lc0(outputStream);
        byte[] opData = outputStream.toByteArray();

        EV3OperationPackage opPkg = new EV3OperationPackage(OpCode.INPUT_READY, opData);
        return new EV3Operation(opPkg );
    }

    /**
     * reading specific device and mode in SI units
     *
     * @param layer (Data8) LAYER – Specify chain layer number [0 - 3]
     * @param no (Data8) NO – Port number
     * @param type  (Data8) TYPE – Specify device type (0 = Don’t change type)
     * @param mode (Data8) MODE – Device mode [0 - 7] (-1 = Don’t change mode)
     * @param si (DataF) SI – SI unit value from device
     * @return
     */
    public static EV3Operation opInputReadSI(
            Layer layer,
            InputPort no,
            Data8 type,
            Data8 mode,
            ReturnParam<DataF> si) throws IOException {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

        LCEncoder.from(layer).lc0(outputStream);
        LCEncoder.from(no).lc0(outputStream);
        LCEncoder.from(type).lcx(outputStream);
        LCEncoder.from(mode).lcx(outputStream);
        GVEncoder.from(si.getIndex()).gvx(outputStream);
        byte[] opData = outputStream.toByteArray();

        EV3OperationPackage opPkg = new EV3OperationPackage(OpCode.INPUT_READSI, opData, si.getSize());
        return new EV3Operation(opPkg, Arrays.asList(si) );
    }

    /**
     * reading multiple data from external device simultanously
     *
     * @param layer (Data8) LAYER – Specify chain layer number [0 - 3]
     * @param no (Data8) NO – Port number
     * @param type (Data8) TYPE – Specify device type (0 = Don’t change type)
     * @param mode (Data8) MODE – Device mode [0 - 7] (-1 = Don’t change mode)
     * @param format (Data8) FORMAT – Format selection (PCT, RAW, SI,… )
     * @param values (Data8) Values – Number of return values
     * @param valueArray (FORMAT) VALUE1 – First value received from device in specified mode
     * Return (Depending on number of data samples requested in (VALUES))
     */
    public static EV3Operation opInputReadExt(
            Layer layer,
            InputPort no,
            Data8 type,
            Data8 mode,
            Data8 format,
            Data8 values,
            ReturnParam<?> valueArray) throws IOException {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

        LCEncoder.from(layer).lc0(outputStream);
        LCEncoder.from(no).lc0(outputStream);
        LCEncoder.from(type).lcx(outputStream);
        LCEncoder.from(mode).lcx(outputStream);
        LCEncoder.from(format).lcx(outputStream);
        LCEncoder.from(values).lcx(outputStream);
        GVEncoder.from(valueArray.getIndex()).gvx(outputStream);
        byte[] opData = outputStream.toByteArray();

        EV3OperationPackage opPkg = new EV3OperationPackage(OpCode.INPUT_READEXT, opData, valueArray.getSize());
        return new EV3Operation(opPkg, Arrays.asList(valueArray) );
    }

    /**
     * enables writting data to extenal digital devices
     *
     * @param layer (Data8) LAYER – Specify chain layer number [0 - 3]
     * @param no  (Data8) NO – Port number
     * @param bytes (Data8) BYTES – No of bytes to write [1 - 32]
     * @param data (Data8) DATA – First byte of data8 array to write
     */
    public static EV3Operation opInputWrite(
            Layer layer,
            InputPort no,
            Data8 bytes,
            Data8[] data) throws IOException {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

        LCEncoder.from(layer).lc0(outputStream);
        LCEncoder.from(no).lc0(outputStream);
        LCEncoder.from(bytes).lcx(outputStream);
        for (int i=0; i<data.length; i++) {
            LCEncoder.from(data[i]).lcx(outputStream);
        }

        byte[] opData = outputStream.toByteArray();

        EV3OperationPackage opPkg = new EV3OperationPackage(OpCode.INPUT_WRITE, opData );
        return new EV3Operation(opPkg );
    }

}
